<?php
/**
 * The implementation of the singleton design pattern.
 * 
 * @package Framework
 * @subpackage Core
 * @author Agent Cooper <twinpeaks.agent.cooper@gmail.com>
 */
namespace TwinPeaks\Core;

/**
 * @var array holds all created objects.
 */
$objectPool = [];

trait TSingleton
{
	
	/**
	 * Do not allow creating object by the new operator.
	 * 
	 * @return void
	 */
	final private function __construct() { }
	
	/**
	 * Do not allow cloning object.
	 * 
	 * @return void
	 */
	final private function __clone() { }
	
	/**
	 * Сalled during class instantiation.
	 * 
	 * @return void
	 */
	protected function onCreate() { }
	
	/**
	 * Returns true if child class has a parent specified by the mask.
	 * 
	 * @param mixed $child Class name or object
	 * @param string $parentMask Any substring of parent's class name
	 * 
	 * @return boolean
	 */
	static function hasParentClass($child, $parentMask)
	{
		$currentClass = get_parent_class($child);
		if (!$currentClass)
			return false;

		do
		{
			if (strpos($currentClass, $parentMask) !== false)
				return true;
		}
		while ($currentClass = get_parent_class($currentClass));
		
		return false;
	}
	
	/**
	 * Returns instance of child class using its parent' class name specified
	 * by the mask. Always returns an array.
	 * 
	 * @param string $parentMask Any substring of parent's fully qualified class name.
	 * @return array|null
	 */
	final static function getObjectByParent($parentMask)
	{
		global $objectPool;
		
		foreach ($objectPool as $class => $container)
			if (self::hasParentClass($class, $parentMask))
				return array_values($container);
		
		return null;
	}
	
	/**
	 * Finds object(s) by the mask of its(their) parent's class namе. If not
	 * found the method will create it. Always returns an array.
	 * 
	 * @param string $parentMask Any substring of parent's fully qualified class name
	 * @param array $initArgs Array of arguments to pass to ::getInstance method
	 * 
	 * @return array|null
	 */
	final static function getObjectByParentSafe($parentMask, $args = [])
	{
		$child = self::getObjectByParent($parentMask);
		if ($child !== null)
			return $child;
		
		// Look up all declared classes.
		$result = [];
		foreach (get_declared_classes() as $class)
		{
			if (self::hasParentClass($class, $parentMask))
			{
				$result[] = call_user_func_array(($class . '::getInstance'), $args);
			}
		}

		return count($result) ? $result : null;
	}
	
	/**
	 * Returns array of child objects of the current class.
	 * 
	 * @see TSingleton::getObjectByParent
	 * @return array|null
	 */
	final static function getMyChild()
	{
		return self::getObjectByParent(get_called_class());
	}
	
	/**
	 * Safe variant of ::getMyChild. Returns array of child objects even if they
	 * were not created before.
	 * 
	 * @see TSingleton::getObjectByParentSafe
	 * @return array
	 */
	final static function getMyChildSafe()
	{
		$args = func_get_args();
		return self::getObjectByParentSafe(get_called_class(), $args);
	}
  
	/**
	 * Hook on the ::getInstance method.
	 * 
	 * @return CSingleton
	 */
	protected function getInstanceHook($instance)
	{
		return $instance;
	}

	/**
	 * Returns class instance.
	 * 
	 * @return TSingleton
	 */
	static function getInstance()
	{
		global $objectPool;

		$argsArray = func_get_args();
		$class = get_called_class();

		if (static::useParametricInstantiation() && count($argsArray))
		{
			$fingerprint = '';
			foreach ($argsArray as $arg)
				$fingerprint .= serialize($arg);
			$key = md5($class . $fingerprint);
		}
		else // Use class name as a key.
			$key = $class;		
		
		if (!isset($objectPool[$class]))
			$objectPool[$class] = [];
		
		if (isset($objectPool[$class][$key]))
			return $objectPool[$class][$key];
    
		$instance = new $class();
		$newInstance = $instance->getInstanceHook($instance);
		
        // Add instance to the objects pool.
		$objectPool[$class][$key] = $newInstance;

		// Initialize object and call user-level initialization routine.
		call_user_func_array([$instance, 'onCreate'], $argsArray);
		return $newInstance;
	}

	/**
	 * Enables or disables the parametric class instantiation. Disabled by default.
	 * 
	 * @return boolean
	 */
	static protected function useParametricInstantiation()
	{
		return false;	
	}
}
